package quarto.controller.server;

import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;

import quarto.Message;
import quarto.controller.Game;
import quarto.controller.client.ClientHandler;
import quarto.view.server.ServerUI;

/**
 * Dit Server object wordt aangemaakt door een ServerUI (TUI of GUI). Het start
 * een ServerSocket op, op het meegegeven port adres. In een loop wordt gekeken
 * of er verbindingen gemaakt worden op de ServerSocket. De Server maakt dan een 
 * ClientHandler aan. Zodra er twee (of meer) ClientHandlers zijn, wordt er een
 * nieuw spel opgestart.
 * 
 * @knows
 * - ServerUI en ClientHandlers
 * @does
 * - Ontvangt verbindingen van clients
 * - Creeert ClientHandlers
 * - Creeert een Game met twee ClientHandlers
 * - Ontvangt informatie van Game en stuurt dit door naar de ServerUI
 * @author Frank Borgonjen & Tim Smit
 */
public class Server extends Thread
{
    /** de server user interface */
	private ServerUI ui;
    
    /** de poort waarop de server gaat luisteren */
	private int port;
    
    /** het socket waar de server op draait */
    private ServerSocket serverSocket;
    
    /** lijst van de clientHandlers */
	private ArrayList<ClientHandler> clientHandlers;
    
    /** lijst van de games */
	private ArrayList<Game> games;
	
	/**
	 * Construeert een nieuw Server-object
	 * @require port > 0 && ui != null
	 * @param port poortnummer waarop geluisterd moet worden
	 * @param ui UserInterface waar server status naartoe gestuurd kan worden
	 */
	public Server(int port, ServerUI ui)
	{
		this.ui             = ui;
		this.port           = port;
		this.clientHandlers = new ArrayList<ClientHandler>();
        this.games          = new ArrayList<Game>();
	}
	
	/**
	 * Wordt aangeroepen bij het starten van de Thread
	 * Maakt een ServerSocket aan op het bekende portnummer en luistert
	 * of er nieuwe verbindingen gemaakt worden.
	 */
    @Override
	public void run()
	{
		this.serverSocket = null;
        
        // probeer een nieuw server socket aan te maken
		try
		{
			this.serverSocket = new ServerSocket(this.port);
			this.addMessage(String.format(Message.SERVER_NOTICE_LISTENING, this.getHostAddress(), this.port));
		}
        // poort is al in gebruik
        catch (BindException e)
        {
            this.errorMessage(String.format(Message.SERVER_ERROR_PORT_IN_USE, this.port));
        }
        // fout bij maken van socket
		catch(IOException e)
		{
			this.errorMessage(Message.SERVER_ERROR_SOCKET);
		}
        // poort buiten bereik
        catch (IllegalArgumentException e)
        {
            this.errorMessage(Message.CLIENT_ERROR_OUT_OF_RANGE);
        }
        
        // er is een server aangemaakt
        if (this.serverSocket != null)
        {
            try
            {
                while (true)
                {
                    // Alle binnenkomende clientSockets accepteren en clientHandler mee aanmaken.
                    // Zodra het aanmaken van een ClientHandler fout gaat, wordt er een
                    // IOException gegooid.
                    Socket clientSocket = this.serverSocket.accept();
                    ClientHandler ch = new ClientHandler(clientSocket, this);

                    this.addMessage(String.format(Message.SERVER_PLAYER_CONNECTED, ch.getClientName()));
                    ch.start();

                    // Toevoegen aan de ClientHandler-lijst
                    this.addClientHandler(ch);

                    // Zodra er twee of meer clientHandlers zijn, gaan we een game aanmaken
                    // ClientHandlers zijn
                    if (this.clientHandlers.size() >= 2)
                    {
                        makeGame();
                    }

                    try
                    {
                        Thread.sleep(500);
                    }
                    catch(InterruptedException e) {}
                }
            }
            catch (IOException e)
            {
                // serverUI op de hoogte brengen van de fout
                this.errorMessage(e.getMessage());
            }
        }
	}
    
    /**
     * Zijn we verbonden
     * @return server status
     */
    public boolean connected()
    {
        return this.serverSocket != null;
    }
	
	/**
	 * Maakt een nieuwe Game aan zodra er twee ClientHandlers zijn die nog niet 
	 * in een Game zitten.
	 * @require getClientHandlers().size() >= 2
	 * @ensure new.getClientHandlers().size() = old.getClientHandlers().size() - 2
	 */
	private void makeGame()
	{
        ArrayList<ClientHandler> chs = getClientHandlers();
        
        // De twee clientHandlers random selecteren
		ClientHandler ch1 = chs.get((int)(Math.random() * chs.size()));
		ClientHandler ch2 = null;
        
        // zolang ch2 nog niet gekozen is, of zolang het gelijk is aan ch1
        // gaan we een random ClientHandler zoeken voor ch2
        while (ch2 == null || ch2 == ch1)
        {
            ch2 = chs.get((int)(Math.random() * chs.size()));
        }
        
        // nieuwe game aanmaken
        Game g = new Game(this, ch1, ch2);
        
        // het Game Object dient als Server en moet dus bekend zijn bij de
        // ClientHandlers.
        ch1.setGame(g);
        ch2.setGame(g);
        
        // clientHandlers verwijderen uit de lijst zodat ze niet in
        // een ander game terecht kunnen komen
        removeClientHandler(ch1);
        removeClientHandler(ch2);
        
        // Even zeggen dat het spel begonnen is
        addMessage(String.format(Message.SERVER_GAME_STARTED, ch1.getClientName(), ch2.getClientName()));
        
        games.add(g);
	}
	
    /**
     * Geeft de lijst van clientHandlers terug
     * @return lijst van clientHandlers
     */
    private ArrayList<ClientHandler> getClientHandlers()
    {
        return this.clientHandlers;
    }
	
	/**
	 * Stuurt een serverbericht door naar de ServerUI.
	 * Berichten kunnen worden verstuurd uit de Game Objecten
	 * @param msg het bericht dat doorgestuurd wordt
	 */
	public void addMessage(String msg)
	{
		this.ui.addMessage(msg);
	}
    
    /**
     * Stuurt een foutmelding naar de ServerUI
     * @param msg de foutmelding
     */
    public void errorMessage(String msg)
    {
        this.ui.errorMessage(msg);
    }
	
	/**
	 * Voegt een ClientHandler toe aan de lijst
	 * @param clientHandler de ClientHandler die toegevoegd wordt
     * @ensure new.getClientHandlers().size() = old.getClientHandlers().size() + 1
	 */
	private void addClientHandler(ClientHandler clientHandler)
	{
		this.clientHandlers.add(clientHandler);
	}
    
    /**
	 * Verwijdert de meegegeven ClientHandler uit de lijst
	 * @param  clientHandler de ClientHandler die verwijderd moet worden
     * @ensure new.getClientHandlers().size() = old.getClientHandlers().size() - 1;
	 */
	public void removeClientHandler(ClientHandler clientHandler)
	{
		this.clientHandlers.remove(clientHandler);
	}
    
    /**
     * Wordt aangeroepen zodra de server afgesloten wordt.
     * De games zullen aan de clients doorgeven dat de server afgesloten
     * is, en geven door dat het een remise is.
     */
    public void closeGames()
    {
        for (Game g : games)
        {
            if (g != null)
            {
                g.closeGame();
            }
        }
    }
    
    /** Levert het Internetadres van deze computer op. */
    private String getHostAddress()
    {
        try
        {
            InetAddress iaddr = InetAddress.getLocalHost();
            return iaddr.getHostAddress();
        }
        catch (UnknownHostException e)
        {
            return "?unknown?";
        }
    }
}
